/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JSplitPane;
import javax.swing.KeyStroke;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.plaf.basic.BasicSplitPaneUI;

/** Graphical convenience methods.
 *
 * <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
 */

public final class OurUtil {

   /** This constructor is private, since this utility class never needs to be instantiated. */
   private OurUtil() { }

   /** Assign the given attributes to the given JComponent, then return the JComponent again.
    * <p> If <b>Font</b>      x is given in the list, we call obj.setFont(x)
    * <p> If <b>String</b>    x is given in the list, we call obj.setToolTipText(x)
    * <p> If <b>Border</b>    x is given in the list, we call obj.setBorder(x)
    * <p> If <b>Dimension</b> x is given in the list, we call obj.setPreferredSize(x)
    * <p> If <b>Color</b> x is given in the list, and it's the first color, we call obj.setForeground(x)
    * <p> If <b>Color</b> x is given in the list, and it's not the first color, we call obj.setBackground(x) then obj.setOpaque(true)
    * <p> (If no Font is given, then after all these changes have been applied, we will call obj.setFont() will a default font)
    */
   public static<X extends JComponent> X make(X obj, Object... attributes) {
      boolean hasFont = false, hasForeground = false;
      if (attributes!=null) for(Object x: attributes) {
         if (x instanceof Font) { obj.setFont((Font)x); hasFont=true; }
         if (x instanceof String) { obj.setToolTipText((String)x); }
         if (x instanceof Border) { obj.setBorder((Border)x); }
         if (x instanceof Dimension) { obj.setPreferredSize((Dimension)x); }
         if (x instanceof Color && !hasForeground) { obj.setForeground((Color)x); hasForeground=true; continue; }
         if (x instanceof Color) { obj.setBackground((Color)x); obj.setOpaque(true); }
      }
      if (!hasFont) obj.setFont(getVizFont());
      return obj;
   }

   /** Make a JLabel, then call Util.make() to apply a set of attributes to it.
    * @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
    */
   public static JLabel label (String label, Object... attributes)  { return make(new JLabel(label), attributes); }

   /** Make a JTextField with the given text and number of columns, then call Util.make() to apply a set of attributes to it.
    * @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
    */
   public static JTextField textfield (String text, int columns, Object... attributes)  {
      return make(new JTextField(text, columns), attributes);
   }

   /** Make a JTextArea with the given text and number of rows and columns, then call Util.make() to apply a set of attributes to it.
    * @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
    */
   public static JTextArea textarea (String text, int rows, int columns, boolean editable, boolean wrap, Object... attributes) {
      JTextArea ans = make(new JTextArea(text, rows, columns), Color.BLACK, Color.WHITE, new EmptyBorder(0,0,0,0));
      ans.setEditable(editable);
      ans.setLineWrap(wrap);
      ans.setWrapStyleWord(wrap);
      return make(ans, attributes);
   }

   /** Make a JScrollPane containing the given component (which can be null), then apply a set of attributes to it.
    * @param attributes - see {@link edu.mit.csail.sdg.alloy4.OurUtil#make OurUtil.make(component, attributes...)}
    */
   public static JScrollPane scrollpane (Component component, Object... attributes) {
      JScrollPane ans = make(new JScrollPane(), new EmptyBorder(0,0,0,0));
      if (component!=null) ans.setViewportView(component);
      ans.setMinimumSize(new Dimension(50, 50));
      return make(ans, attributes);
   }

   /** Returns the recommended font to use in the visualizer, based on the OS. */
   public static Font getVizFont()  {
      return Util.onMac() ? new Font("Lucida Grande", Font.PLAIN, 11) : new Font("Dialog", Font.PLAIN, 12);
   }

   /** Returns the screen width (in pixels). */
   public static int getScreenWidth()  { return Toolkit.getDefaultToolkit().getScreenSize().width; }

   /** Returns the screen height (in pixels). */
   public static int getScreenHeight()  { return Toolkit.getDefaultToolkit().getScreenSize().height; }

   /** Make a graphical button
    * @param label - the text to show beneath the button
    * @param tip - the tooltip to show when the mouse hovers over the button
    * @param iconFileName - if nonnull, it's the filename of the icon to show (it will be loaded from an accompanying jar file)
    * @param func - if nonnull, it's the function to call when the button is pressed
    */
   public static JButton button (String label, String tip, String iconFileName, ActionListener func) {
      JButton button = new JButton(label, (iconFileName!=null && iconFileName.length()>0) ? loadIcon(iconFileName) : null);
      if (func != null) button.addActionListener(func);
      button.setVerticalTextPosition(JButton.BOTTOM);
      button.setHorizontalTextPosition(JButton.CENTER);
      button.setBorderPainted(false);
      button.setFocusable(false);
      if (!Util.onMac()) button.setBackground(new Color(0.9f, 0.9f, 0.9f));
      button.setFont(button.getFont().deriveFont(10.0f));
      if (tip != null && tip.length() > 0) button.setToolTipText(tip);
      return button;
   }

   /** Load the given image file from an accompanying JAR file, and return it as an Icon object. */
   public static Icon loadIcon(String pathname) {
      URL url = OurUtil.class.getClassLoader().getResource(pathname);
      if (url!=null) return new ImageIcon(Toolkit.getDefaultToolkit().createImage(url));
      return new ImageIcon(new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB));
   }

   /** Make a JPanel with horizontal or vertical BoxLayout, then add the list of components to it (each aligned by xAlign and yAlign)
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   private static JPanel makeBox(boolean horizontal, float xAlign, float yAlign, Object[] components) {
      JPanel ans = new JPanel();
      ans.setLayout(new BoxLayout(ans, horizontal ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS));
      ans.setAlignmentX(0.0f);
      ans.setAlignmentY(0.0f);
      Color color = null;
      for(Object x: components) {
         Component c = null;
         if (x instanceof Color) { color = (Color)x; ans.setBackground(color); continue; }
         if (x instanceof Dimension) { ans.setPreferredSize((Dimension)x); ans.setMaximumSize((Dimension)x); continue; }
         if (x instanceof Component) { c = (Component)x; }
         if (x instanceof String) { c = label((String)x, Color.BLACK); }
         if (x instanceof Integer) { int i = (Integer)x; c = Box.createRigidArea(new Dimension(horizontal?i:1, horizontal?1:i)); }
         if (x==null) { c = horizontal ? Box.createHorizontalGlue() : Box.createVerticalGlue(); }
         if (c==null) continue;
         if (color!=null) c.setBackground(color);
         if (c instanceof JComponent) { ((JComponent)c).setAlignmentX(xAlign); ((JComponent)c).setAlignmentY(yAlign); }
         ans.add(c);
      }
      return ans;
   }

   /** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be center-aligned).
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   public static JPanel makeH(Object... components) { return makeBox(true, 0.5f, 0.5f, components); }

   /** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be top-aligned).
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   public static JPanel makeHT(Object... components) { return makeBox(true, 0.5f, 0.0f, components); }

   /** Make a JPanel using horizontal BoxLayout, and add the components to it (each component will be bottom-aligned).
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   public static JPanel makeHB(Object... components) { return makeBox(true, 0.5f, 1.0f, components); }

   /** Make a JPanel using vertical BoxLayout, and add the components to it (each component will be left-aligned).
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   public static JPanel makeVL(Object... components) { return makeBox(false, 0.0f, 0.5f, components); }

   /** Make a JPanel using vertical BoxLayout, and add the components to it (each component will be right-aligned).
    * <br> If a component is Color, it's the background of this JPanel and every component after it (until we see another Color)
    * <br> If a component is Dimension, we will set it as the newly constructed JPanel's preferedSize and MaximumSize.
    * <br> If a component is String, we will insert a JLabel with it as the label.
    * <br> If a component is Integer, we will insert an "n*1" (or "1*n") rigid area instead.
    * <br> If a component is null, we will insert a horizontal (or vertical) glue instead.
    */
   public static JPanel makeVR(Object... components) { return makeBox(false, 1.0f, 0.5f, components); }

   /** Constructs a new SplitPane containing the two components given as arguments
    * @param orientation - the orientation (HORIZONTAL_SPLIT or VERTICAL_SPLIT)
    * @param first - the left component (if horizontal) or top component (if vertical)
    * @param second - the right component (if horizontal) or bottom component (if vertical)
    * @param initialDividerLocation - the initial divider location (in pixels)
    */
   public static JSplitPane splitpane (int orientation, Component first, Component second, int initialDividerLocation) {
      JSplitPane x = make(new JSplitPane(orientation, first, second), new EmptyBorder(0,0,0,0));
      x.setContinuousLayout(true);
      x.setDividerLocation(initialDividerLocation);
      x.setOneTouchExpandable(false);
      x.setResizeWeight(0.5);
      if (Util.onMac() && (x.getUI() instanceof BasicSplitPaneUI)) {
         boolean h = (orientation != JSplitPane.HORIZONTAL_SPLIT);
         ((BasicSplitPaneUI)(x.getUI())).getDivider().setBorder(new OurBorder(h,h,h,h));  // Makes the border look nicer on Mac OS X
      }
      return x;
   }

   /** Convenience method that recursively enables every JMenu and JMenuItem inside "menu".
    * @param menu - the menu to start the recursive search
    */
   public static void enableAll (JMenu menu) {
      for(int i = 0; i < menu.getMenuComponentCount(); i++) {
         Component x = menu.getMenuComponent(i);
         if (x instanceof JMenuItem) ((JMenuItem)x).setEnabled(true); else if (x instanceof JMenu) enableAll((JMenu)x);
      }
   }

   /** Construct a new JMenu and add it to an existing JMenuBar.
    * <p> Note: every time the user expands then collapses this JMenu, we automatically enable all JMenu and JMenuItem objects in it.
    *
    * @param parent - the JMenuBar to add this Menu into (or null if we don't want to add it to a JMenuBar yet)
    * @param label - the label to show on screen (if it contains '&' followed by 'a'..'z', we'll remove '&' and use it as mnemonic)
    * @param func - if nonnull we'll call its "run()" method right before expanding this menu
    */
   public static JMenu menu (JMenuBar parent, String label, final Runnable func) {
      final JMenu x = new JMenu(label.replace("&", ""), false);
      if (!Util.onMac()) {
         int i = label.indexOf('&');
         if (i>=0 && i+1<label.length()) i = label.charAt(i+1);
         if (i>='a' && i<='z') x.setMnemonic((i-'a')+'A'); else if (i>='A' && i<='Z') x.setMnemonic(i);
      }
      x.addMenuListener(new MenuListener() {
         public void menuSelected   (MenuEvent e) { if (func != null) func.run(); }
         public void menuDeselected (MenuEvent e) { OurUtil.enableAll(x); }
         public void menuCanceled   (MenuEvent e) { OurUtil.enableAll(x); }
      });
      if (parent!=null) parent.add(x);
      return x;
   }

   /** Construct a new JMenuItem then add it to an existing JMenu.
    * @param parent - the JMenu to add this JMenuItem into (or null if you don't want to add it to any JMenu yet)
    * @param label - the text to show on the menu
    * @param attrs - a list of attributes to apply onto the new JMenuItem
    * <p> If one positive number  a is supplied, we call setMnemonic(a)
    * <p> If two positive numbers a and b are supplied, and a!=VK_ALT, and a!=VK_SHIFT, we call setMnemoic(a) and setAccelerator(b)
    * <p> If two positive numbers a and b are supplied, and a==VK_ALT or a==VK_SHIFT, we call setAccelerator(a | b)
    * <p> If an ActionListener is supplied, we call addActionListener(x)
    * <p> If an Boolean x      is supplied, we call setEnabled(x)
    * <p> If an Icon x         is supplied, we call setIcon(x)
    */
   public static JMenuItem menuItem (JMenu parent, String label, Object... attrs) {
      JMenuItem m = new JMenuItem(label, null);
      int accelMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
      boolean hasMnemonic = false;
      for(Object x: attrs) {
         if (x instanceof Character || x instanceof Integer) {
            int k = (x instanceof Character) ? ((int)((Character)x)) : ((Integer)x).intValue();
            if (k < 0) continue;
            if (k==KeyEvent.VK_ALT)   { hasMnemonic = true; accelMask = accelMask | InputEvent.ALT_MASK;   continue; }
            if (k==KeyEvent.VK_SHIFT) { hasMnemonic = true; accelMask = accelMask | InputEvent.SHIFT_MASK; continue; }
            if (!hasMnemonic) { m.setMnemonic(k); hasMnemonic=true; } else m.setAccelerator(KeyStroke.getKeyStroke(k, accelMask));
         }
         if (x instanceof ActionListener) m.addActionListener((ActionListener)x);
         if (x instanceof Icon) m.setIcon((Icon)x);
         if (x instanceof Boolean) m.setEnabled((Boolean)x);
      }
      if (parent!=null) parent.add(m);
      return m;
   }

   /** This method minimizes the window. */
   public static void minimize(JFrame frame) { frame.setExtendedState(JFrame.ICONIFIED); }

   /** This method alternatingly maximizes or restores the window. */
   public static void zoom(JFrame frame) {
      int both = JFrame.MAXIMIZED_BOTH;
      frame.setExtendedState((frame.getExtendedState() & both)!=both ? both : JFrame.NORMAL);
   }

   /** Make the frame visible, non-iconized, and focused. */
   public static void show(JFrame frame) {
      frame.setVisible(true);
      frame.setExtendedState(frame.getExtendedState() & ~JFrame.ICONIFIED);
      frame.requestFocus();
      frame.toFront();
   }
}
